<?php
/**
 * This file is part of the Sphoof framework.
 * Copyright (c) 2010-2011 Sphoof
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code. You can also view the
 * LICENSE file online at http://www.sphoof.nl/new-bsd.txt
 *
 * @category	Sphoof
 * @copyright	Copyright (c) 2010-2011 Sphoof (http://sphoof.nl)
 * @license		http://sphoof.nl/new-bsd.txt	New BSD License
 * @package		Http
 */

/**
 * An exception that will be thrown in case of failure while setting a cookie.
 *
 * @package		Http
 * @subpackage	Exception
 */
class SpSetCookieException extends SpException { }

/**
 * Represents a HTTP request.
 *
 * @todo Decent file upload handling support
 *
 * @package Http
 */
class SpHttpRequest {
	/**
	 * Contains all the globals.
	 *
	 * @var Array
	 */
	protected $globals;

	/**
	 * Contains a SpUrl object that represents the current URL.
	 *
	 * @var SpUrl
	 */
	protected $url;

	/**
	 * Contains a SpSession object that holds the session information.
	 *
	 * @var SpSession
	 */
	protected $session;

	/**
	 * Contains a SpCookies object that holds the cookie information.
	 *
	 * @var SpCookies
	 */
	protected $cookies;

	/**
	 * Constructs a new HTTP request.
	 */
	public function __construct( SpSession $session = null, SpCookies $cookies = null ) {
		$this->globals = array(
			'query' => $_GET,
			'body' => $_POST,
			'server' => $_SERVER,
			'files' => isset( $_FILES ) ? $this->normalise( $_FILES ) : array( )
		);
		$this->session = isset( $session ) ? $session : new SpSession;
		$this->cookies = isset( $cookies ) ? $cookies : new SpCookies;
	}

	/**
	 * Returns a URL object of the current Request.
	 *
	 * @return SpUrl
	 */
	public function url( ) {
		if( null === $this->url ) {
			$this->url = new SpUrl(
				$this->server( 'HTTP_HOST' ),
				$this->server( 'REQUEST_URI' ),
				$this->server( 'SCRIPT_NAME' )
			);
		}
		return $this->url;
	}

	/**
	 * Retrieves a specific value from the session. If $key is null, the whole session object will be returned.
	 * If the key is not found within the session, the value of $default will be returned.
	 *
	 * @param mixed $key
	 * @param mixed $default
	 * @return mixed
	 */
	public function session( $key = null, $default = null ) {
		return ( $key ? $this->session->get( $key, $default ) : $this->session );
	}

	/**
	 * Retrieves a specific value from the cookies. If $key is null, the whole cookie object will be returned.
	 * If the key is not found within the cookies, the value of $default will be returned.
	 *
	 * @param mixed $key
	 * @param mixed $default
	 * @return mixed
	 */
	public function cookies( $key = null, $default = null ) {
		return ( $key ? $this->cookies->get( $key, $default ) : $this->cookies );
	}

	/**
	 * Retrieves a specific value from the request's query. If $key is null, the whole query array will be returned.
	 * If the key is not found within the query, the value of $default will be returned.
	 *
	 * @param mixed $key
	 * @param mixed $default
	 * @return mixed
	 */
	public function query( $key = null, $default = null ) {
		return $this->getParameter( $key, $default, $this->globals['query'] );
	}

	/**
	 * Retrieves a specific value from the request's body. If $key is null, the whole body array will be returned.
	 * If the key is not found within the body, the value of $default will be returned.
	 *
	 * @param mixed $key
	 * @param mixed $default
	 * @return mixed
	 */
	public function body( $key = null, $default = null ) {
		return $this->getParameter( $key, $default, $this->globals['body'] );
	}

	/**
	 * Retrieves a specific value from the request's file array. If $key is null,
	 * the whole file array will be returned. If the key is not found within the
	 * file array, the value of $default will be returned.
	 *
	 * @param mixed $key
	 * @param mixed $default
	 * @return mixed
	 */
	public function files( $key = null, $default = null ) {
		return $this->getParameter( $key, $default, $this->globals['files'] );
	}

	/**
	 * Retrieves a specific value from the server information. If $key is null, the whole array of server values will be returned.
	 * If the key is not found within the server, the value of $default will be returned.
	 *
	 * @param mixed $key
	 * @param mixed $default
	 * @return mixed
	 */
	public function server( $key = null, $default = null ) {
		return $this->getParameter( $key, $default, $this->globals['server'] );
	}

	/**
	 * Returns the current requests' HTTP method.
	 *
	 * @return string
	 */
	public function method( ) {
		return $this->server( 'REQUEST_METHOD' );
	}

	/**
	 * Returns a value from the array. If $key is null, the whole array is returned. If there is no value
	 * for the key, the value of $default will be returned.
	 *
	 * @param mixed $key
	 * @param mixed $default
	 * @param Array $values
	 */
	protected function getParameter( $key, $default, Array $values ) {
		return ( $key ? ( isset( $values[$key] ) ? $values[$key] : $default ) : $values );
	}

	/**
	 * Creates an array of SpUploadedFile instances. If there are multiple files
	 * in the same http-body key, the files array will also have an array of
	 * SpUploadedFiles instances.
	 *
	 * @param array $files
	 * @return Array
	 */
	protected function normalise( Array $files ) {
		$uploaded = array( );
		foreach( $files as $index => $file ) {
			$uploaded[$index] = is_array( $file['name'] ) ? $this->splitFiles( $file ) : $this->file( $file );
		}
		return $uploaded;
	}

	/**
	 * Creates and returns an instance of SpUploadedFile.
	 *
	 * @param array $file
	 * @return SpUploadedFile
	 */
	protected function file( Array $file ) {
		return new SpUploadedFile( $file['name'], $file['type'], $file['tmp_name'], $file['error'], $file['size'] );
	}

	/**
	 * If there are multiple files in the same index, this method will split the
	 * information into multiple SpUploadedFiles instances.
	 *
	 * @param array $files
	 * @return array
	 */
	protected function splitFiles( Array $files ) {
		$uploaded = array( );
		while( count( $files['name'] ) > 0 ) {
			$file = array(
				'name' => array_shift( $files['name'] ),
				'type' => array_shift( $files['type'] ),
				'tmp_name' => array_shift( $files['tmp_name'] ),
				'error' => array_shift( $files['error'] ),
				'size' => array_shift( $files['size'] )
			);
			$uploaded[] = $this->file( $file );
		}
		return $uploaded;
	}
}

/**
 * Holds session data for reading and writing.
 *
 * @package Http
 */
class SpSession {
	/**
	 * Sets the value of session variable $key to $value and will overwrite any existing value.
	 *
	 * @param string $key
	 * @param mixed $value
	 * @return SpSession
	 */
	public function set( $key, $value ) {
		if( !$this->started( ) ) {
			$this->start( );
		}
		$_SESSION[$key] = $value;
		return $this;
	}

	/**
	 * Removes the session variable $key.
	 *
	 * @param string $key
	 * @return SpSession
	 */
	public function remove( $key ) {
		unset( $_SESSION[$key] );
		return $this;
	}

	/**
	 * Finds and returns the value of session variable $key if it exists or
	 * $default if it doesn't.
	 *
	 * @param string $key
	 * @param mixed $default
	 * @return mixed
	 */
	public function get( $key = null, $default = null ) {
		if( !$this->started( ) ) {
			$this->start( );
		}
		return isset( $_SESSION[$key] ) ? $_SESSION[$key] : $default;
	}

	protected function started( ) {
		return '' !== session_id( );
	}

	protected function start( ) {
		return session_start( );
	}
}

/**
 * Holds cookie data for reading and writing.
 *
 * @package Http
 * @todo factor out the ugly cookiesetter and use a virtual request instead?
 */
class SpCookies {
	/**
	 * Contains the current data in the cookies.
	 *
	 * @var Array
	 */
	protected $cookies = array( );

	/*
	 * Contains the function name for setting cookies, so we can override it
	 * when unit testing
	 *
	 * @var String
	 */
	protected $cookiesetter = 'setcookie';

	/**
	 *
	 */
	public function __construct( ) {
		$this->cookies = $_COOKIE;
	}

	/**
	 * Sets a cookie with the name $key.
	 *
	 * @param string $key
	 * @param mixed $value
	 * @param int $lifetime in seconds
	 * @return SpCookies
	 */
	public function set( $key, $value, $lifetime = 1000000 ) {
		$result = call_user_func($this->cookiesetter, $key, $value, time( ) + $lifetime );
		if ( false == $result ) {
			throw new SpSetCookieException( 'Failed to set cookie ' . $key );
		}
		$this->cookies[$key] = $value;
		return $this;
	}

	/**
	 * Removes the cookie known by $key.
	 *
	 * @param string $key
	 * @return SpCookies
	 */
	public function remove( $key ) {
		if ( isset($this->cookies[$key]) ) {
			$result = call_user_func($this->cookiesetter, $key, '', time( ) - 1000 );
			if ( false == $result ) {
				throw new SpSetCookieException( 'Failed to unset cookie ' . $key );
			}
			unset( $this->cookies[$key] );
		}
		return $this;
	}

	/**
	 * Finds the cookie known as $key. If it doesn't exist, $default will be returned.
	 * @param string $key
	 * @param string $default
	 * @return SpCookies
	 */
	public function get( $key, $default = null ) {
		return isset( $this->cookies[$key] ) ? $this->cookies[$key] : $default;
	}
}

/**
 * The response that a controller should pass to the bootstrap to display.
 *
 * @package Http
 */
class SpHttpResponse {
	/**
	 * An array of headers that should be sent.
	 *
	 * @var array
	 */
	protected $headers = array( );

	/**
	 * The body of the response.
	 *
	 * @var string
	 */
	protected $body;

	/**
	 * The responsecode.
	 *
	 * @var int
	 */
	protected $responsecode = 200;

	/**
	 * Function that will actually send the headers.
	 *
	 * @var string
	 * @todo factor this out, if at all possible.
	 */
	protected $headersender = 'header';

	protected $type;

	/**
	 * Constructs the response.
	 *
	 * @param string $body
	 * @param int $status
	 */
	public function __construct( $body, $status = 200, $type = 'html' ) {
		$this->responsecode = $status;
		$this->body = $body;
		$this->type = $type;
	}

	public function body( ) {
		return $this->body;
	}

	public function hasHeader( $key, $content = null ) {
		foreach ( $this->headers as $header ) {
			if ( strtolower( $header['key'] ) === strtolower( $key ) ) {
				if( null === $content or $header['content'] === $content ) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Outputs all headers and the body subsequently.
	 *
	 * @return boolean
	 */
	public function output( ) {
		$this->sendHeaders( );
		$this->sendBody( );
	}

	/**
	 * Adds a header to the header stack.
	 *
	 * @param string $key
	 * @param string $value
	 * @return SpResponse
	 */
	public function header( $key, $value, $replace = true ) {
		$this->headers[] = array(
			'key' => ucfirst( $key ),
			'content' => $value,
			'replace' => $replace
		);
		return $this;
	}

	/**
	 * Sets the body.
	 *
	 * @param string $body
	 * @return SpResponse
	 * @todo Determine if this method is actually necessary.
	 */
	public function setBody( $body ) {
		$this->body = $body;
		return $this;
	}

	/**
	 * Sends all headers.
	 *
	 * @return boolean
	 */
	protected function sendHeaders( ) {
		foreach( (array) $this->headers as $header ) {
			call_user_func(
				$this->headersender,
				sprintf( '%s: %s', $header['key'], $header['content'] ),
				$header['replace'], $this->responsecode
			);
		}
		return true;
	}

	/**
	 * Outputs the body.
	 */
	protected function sendBody( ) {
		echo $this->body;
	}
}

/**
 * A redirecting response.
 *
 * @package Http
 */
class SpRedirectingResponse extends SpHttpResponse {
	/**
	 * Construct the redirecting response.
	 *
	 * @param string $url
	 * @param int $status
	 */
	public function __construct( $url, $status = 303 ) {
		$this->header( 'location', $url );
		$this->responsecode = $status;
	}

	/**
	 * Overwrites the default output and only sends the headers, including the
	 * redirecting header.
	 */
	public function output( ) {
		$this->sendHeaders( );
	}
}

/**
 * Represents a URL.
 *
 * @package Http
 */
class SpUrl {
	/**
	 * The hostname
	 * @var String
	 */
	protected $host;

	/**
	 * The full path, including the current filename.
	 * @var String
	 */
	protected $fullpath;

	/**
	 * The file that is being executed.
	 * @var String
	 */
	protected $file;

	/**
	 * The path relative from the current file.
	 * @var String
	 */
	protected $relativepath;

	/**
	 * The extension or filetype requested.
	 * @var String
	 */
	protected $extension;

	/**
	 * @param String $host
	 * @param String $fullpath
	 * @param String $file
	 */
	public function __construct( $host, $fullpath, $file ) {
		$this->host = $host;
		$this->fullpath = $fullpath;
		$this->file = $file;
	}

	/**
	 * Returns the path relative to the folder sphoof was started from.
	 *
	 * @return String
	 */
	public function path( ) {
		if( null === $this->relativepath ) {
			$this->relativepath = parse_url(
				preg_replace( sprintf( '~^(%s|%s)~i', preg_quote( $this->file, '~' ), preg_quote( $this->dirname( ), '~' ) ), '', $this->fullpath ), PHP_URL_PATH
			);
		}
		return '/' . trim( $this->relativepath, '/' );
	}

	/**
	 * Returns the extension or filetype of the request.
	 *
	 * @return String|false
	 */
	public function extension( ) {
		if( null === $this->extension ) {
			$this->extension = ( 0 !== preg_match( '~\.([a-z0-9]{2,4})$~i', $this->path( ), $matches ) ) ? array_pop( $matches ) : false;
		}
		return $this->extension;
	}

	/**
	 * Returns the directory in which the currently executed file is located in.
	 *
	 * @return String
	 */
	public function dirname( ) {
		return dirname( str_replace( array( '\\\\', '\\' ), '/', $this->file ) );
	}

	/**
	 * Returns the hostname for this request.
	 *
	 * @return String
	 */
	public function host( ) {
		return $this->host;
	}

	/**
	 * Returns the full URL of the request.
	 *
	 * @return String
	 */
	public function __toString( ) {
		return 'http://' . $this->host . $this->fullpath;
	}
}

/**
 * Responsetypes are objects that send the appropriate headers for the requested
 * content-type, such as HTML, JSON and XML.
 *
 * @package Http
 */
abstract class SpResponseType {
	/**
	 * An array of headers that should be send with this response-type.
	 *
	 * @var Array
	 */
	protected $headers;

	/**
	 * This method will be called once the response-type is set on the response.
	 *
	 * @return void
	 */
	public function headers( SpHttpResponse $response ) {
		foreach( $this->headers as $header => $content ) {
			$response->header( $header, $content );
		}
		return;
	}
}

/**
 * A response with the HTML content-type.
 *
 * @package Http
 */
class SpHtmlResponse extends SpResponseType {
	protected $headers = array(
		'Content-Type' => 'text/html;charset=UTF-8'
	);
}

/**
 * A response with the JSON content-type.
 *
 * @package Http
 */
class SpJsonResponse extends SpResponseType {
	protected $headers = array(
		'Content-Type' => 'application/json;charset=UTF-8'
	);
}

/**
 * A response with the XML content-type. UTF-8 output is assumed.
 *
 * @package Http
 */
class SpXmlResponse extends SpResponseType {
	protected $headers = array(
		'Content-Type' => 'text/xml;charset=UTF-8'
	);
}

class SpUploadedFile {

	protected $filename;
	protected $type;
	protected $tmpname;
	protected $error;
	protected $size;
	protected $destination;

	public function __construct( $filename, $type, $tmpname, $error, $size ) {
		$this->filename = $filename;
		$this->type = $type;
		$this->tmpname = $tmpname;
		$this->error = $error;
		$this->size = $size;
	}

	public function move( $destination ) {
		$this->destination = is_dir( $destination ) ? ( $destination . $this->filename ) : $destination;
		if( false !== move_uploaded_file( $this->tmpname, $this->destination ) ) { /** Can not be done :( */
			return true;
		}
	}

	public function filename( ) {
		return $this->filename;
	}

	public function destination( ) {
		return $this->destination;
	}

}

